﻿using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Windows.Threading;

namespace Creventive.Wpf.ShellFactory.Threading
{
    /// <summary>
    ///   Creates a worker thread with a running dispatcher.
    /// </summary>
    public class DispatcherThread : IDisposable
    {
        private Dispatcher dispatcher;
        private Thread thread;
        private Collection<DispatcherOperation> operations = new Collection<DispatcherOperation>();

        /// <summary>
        ///   Initializes a new instance of the <see cref = "DispatcherThread" /> class.
        /// </summary>
        /// <param name = "apartmentState">State of the apartment.</param>
        public DispatcherThread(ApartmentState apartmentState)
        {
            this.thread = new Thread(this.Thread_Start);
            this.thread.SetApartmentState(apartmentState);
            lock (this.thread)
            {
                this.thread.Start();
                Monitor.Wait(this.thread);
            }
        }

        /// <summary>
        /// Occurs when an unhandled exception has occurred in the thread.
        /// </summary>
        public event DispatcherUnhandledExceptionEventHandler UnhandledException;

        /// <summary>
        ///   Gets the dispatcher.
        /// </summary>
        /// <value>The dispatcher.</value>
        public Dispatcher Dispatcher
        {
            get { return this.dispatcher; }
        }

        /// <summary>
        ///   Gets the thread.
        /// </summary>
        /// <value>The thread.</value>
        public Thread Thread
        {
            get { return this.thread; }
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="DispatcherThread"/> is reclaimed by garbage collection.
        /// </summary>
        ~DispatcherThread()
        {
            this.Dispose(false, false);
        }

        private void Thread_Start()
        {
            lock (this.thread)
            {
                this.dispatcher = Dispatcher.CurrentDispatcher;
                this.dispatcher.Hooks.OperationPosted += this.Hooks_OperationPosted;
                this.dispatcher.Hooks.OperationCompleted += this.Hooks_OperationCompleted;
                this.dispatcher.Hooks.OperationAborted += this.Hooks_OperationAborted;
                this.dispatcher.UnhandledException += this.dispatcher_UnhandledException;
                Monitor.Pulse(this.thread);
            }
            Dispatcher.Run();
        }

        private void dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
        {
            var handler = this.UnhandledException;
            if (handler != null)
                handler.Invoke(this, e);
        }

        private void Hooks_OperationAborted(object sender, DispatcherHookEventArgs e)
        {
            lock (this.operations)
                this.operations.Remove(e.Operation);
        }

        private void Hooks_OperationCompleted(object sender, DispatcherHookEventArgs e)
        {
            lock (this.operations)
                this.operations.Remove(e.Operation);
        }

        private void Hooks_OperationPosted(object sender, DispatcherHookEventArgs e)
        {
            lock (this.operations)
                this.operations.Add(e.Operation);
        }

        /// <summary>
        ///   Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true, true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        ///   Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name = "ensureOperationsAreCompleted">if set to <c>true</c> ensure operations are completed.</param>
        public void Dispose(bool ensureOperationsAreCompleted)
        {
            this.Dispose(true, ensureOperationsAreCompleted);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        ///   Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name = "disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        /// <param name = "ensureOperationsAreCompleted">if set to <c>true</c> ensure operations are completed.</param>
        protected void Dispose(bool disposing, bool ensureOperationsAreCompleted)
        {
            if (this.dispatcher == null)
                return;

            if (disposing)
            {
                this.dispatcher.Hooks.OperationPosted -= this.Hooks_OperationPosted;
                this.dispatcher.Hooks.OperationCompleted -= this.Hooks_OperationCompleted;
                this.dispatcher.Hooks.OperationAborted -= this.Hooks_OperationAborted;
                if (ensureOperationsAreCompleted)
                {
                    DispatcherOperation[] runningOperations;
                    lock (this.operations)
                        runningOperations = this.operations.ToArray();

                    foreach (DispatcherOperation operation in runningOperations)
                        operation.Wait();
                }
            }
            if (ensureOperationsAreCompleted)
                this.dispatcher.InvokeShutdown();
            else
                this.dispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);
            this.dispatcher = null;
            if (ensureOperationsAreCompleted)
                this.thread.Join();
        }
    }
}